#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys
import configparser
from xdg import BaseDirectory

progname = 'bufe'

# Exit codes: 1 - invocation error, 2 - config file error, 3 - error in external program

def main():
    # Read config file
    config_dir = get_config_dir()
    config_file_name = os.path.join(config_dir, "%s.conf" % (progname))
    defaults ={"type": "mirror",
        "preserve": "False",
        "pre-mandatory": "False",
        "post-mandatory": "False"}
    config = configparser.SafeConfigParser(defaults)
    print("Reading config file %s" % (config_file_name))
    config.read(os.path.join(config_file_name))

    # Parse command line
    if len(sys.argv) != 2:
        print("Usage: %s {-l | backup_name}" % (progname))
        exit(1)
    backup_name = sys.argv[1]

    # Print list of defined backups if -l specified
    if backup_name == "-l":
        print ("The following backups are defined:")
        for s in config.sections():
            print(s)
        exit(0)

    # Check that config exists
    if not config.has_section(backup_name):
        print("%s: Backup named %s not found in config file." %
            (progname, backup_name))
        exit(1)

    # Check that all the right options are in the section
    backup_options = dict(config.items(backup_name))
    backup_options['backup_name'] = backup_name
    backup_options['config_dir'] = config_dir
    if not options_ok(backup_options):
        exit(2)

    # Check that the source, destination and tmp directories exist
    # Expand any ~ (homedir) at the same time
    cumulative_error = False
    backup_options['source'] = expand_path(backup_options['source'])
    if not dir_exists(backup_options['source'], 'Source'):
        cumulative_error = True
    backup_options['destination'] = expand_path(backup_options['destination'])
    if not dir_exists(backup_options['destination'], 'Destination'):
        cumulative_error = True
    if backup_options['type'] == 'incremental':
        backup_options['tmp'] = expand_path(backup_options['tmp'])
        if not dir_exists(backup_options['tmp'], 'Temporary'):
            cumulative_error = True
    if cumulative_error:
        exit(2)

    # Run pre-script
    pre_file = os.path.join(config_dir, backup_name + "-pre")
    if os.path.isfile(pre_file):
        print("Running pre-script %s" % (pre_file))
        exit_on_fail = backup_options['pre-mandatory'].lower() == 'true'
        do_command(pre_file, exit_on_fail)

    # Do backup
    print("Performing backup")
    backup_function = {
        'incremental': rdup_backup,
        'mirror': rsync_mirror,
        'snapshot': rsync_snapshot}
    return_value = backup_function[backup_options['type']](backup_options)
    if return_value != 0:
        print("\nBackup failed")
        if backup_options['post-mandatory'].lower() == 'false':
            exit(2)
    else:
        print("Backup complete")

    # Run the post-script
    post_file = os.path.join(config_dir, backup_name + "-post")
    if os.path.isfile(post_file):
        print("Running post-script %s" % (post_file))
        exit_on_fail = backup_options['post-mandatory'].lower() == 'true'
        do_command(post_file, exit_on_fail)

    return 0


def options_ok(backup_options):
    cumulative_error = False
    if 'type' not in backup_options:
        print("%s: No backup type found for %s." % (progname,
            backup_options['backup_name']))
        cumulative_error = True

    option_list = {
        'incremental': ('type', 'source', 'destination', 'tmp', 'splitsize'),
        'mirror': ('type', 'source', 'destination', 'preserve'),
        'snapshot': ('type', 'source', 'destination')}
    if backup_options['type'] not in option_list:
        print("%s: Invalid backup type %s" %
            (progname, backup_options['type']))
        cumulative_error = True
    else:
        for option in option_list[backup_options['type']]:
            if option not in backup_options:
                print("%s: Option %s not found" % (progname, option))
                cumulative_error = True
    return not cumulative_error


def get_config_dir():
    xdg_config_dir = BaseDirectory.save_config_path(progname)
    return xdg_config_dir


def expand_path(path):
    return path.replace('~', os.path.expanduser('~'))


def dir_exists(directory, name):
    if not os.path.isdir(directory):
        print("%s: %s directory %s does not exist." %
            (progname, name, directory))
        return False
    return True


def rsync_mirror(options):
    print("Performing mirror backup.")

    exclude_file_name = os.path.join(options['config_dir'],
        options['backup_name'] + "-exclude")
    if os.path.isfile(exclude_file_name):
        exclude_string = "--exclude-from='%s'" % (exclude_file_name)
    else:
        exclude_string = ""

    if options['preserve'].lower() == 'false':
        rsync_options = 'rltD' # Don't preserve permissions, owner and group
    else:
        rsync_options = 'a'
    command = ("rsync -%svl %s --delete-before --delete-excluded '%s' '%s'" %
        (rsync_options, exclude_string, options['source'], options['destination']))

    return do_command(command, False)


def rsync_snapshot(options):
    print("Performing snapshot backup.")

    import datetime, dateutil.parser, shutil

    ### delete old stuff
    if 'keep daily' in options:
        keep_daily = dateutil.relativedelta.relativedelta(months=int(options['keep daily']))
    else:
        keep_daily = dateutil.relativedelta.relativedelta(months=3)
    keep_daily = datetime.datetime.now() - keep_daily
    if 'keep weekly' in options:
        keep_weekly = dateutil.relativedelta.relativedelta(years=int(options['keep weekly']))
    else:
        keep_weekly = dateutil.relativedelta.relativedelta(years=2)
    keep_weekly = datetime.datetime.now() - keep_weekly
    # Get a list of backup directories
    dir_list = [d for d in os.listdir(options['destination'])
        if os.path.isdir(os.path.join(options['destination'], d)) == True]
    for d in dir_list:
        try:
            deleted = False
            dir_date = datetime.datetime.strptime(d, '%Y-%m-%d.%H%M%S')
            (year, week, weekday) = dir_date.isocalendar()
            # Delete daily backups older than keep_daily -- keep Mondays.
            if dir_date < keep_daily:
                # if it's not a Monday, delete.
                if weekday != 1:
                    delete_dir = os.path.join(options['destination'], d)
                    print("deleting", delete_dir)
                    shutil.rmtree(delete_dir)
                    deleted = True
            # Delete weekly backups older than keep_weekly -- keep week number if it's zero modulo 4
            if dir_date < keep_weekly and not deleted:
                print(week % 4)
                if week % 4 != 0:
                    delete_dir = os.path.join(options['destination'], d)
                    print("deleting", delete_dir)
                    shutil.rmtree(delete_dir)
        except ValueError:
            pass
    ### do backup
    date_string = datetime.datetime.now().strftime('%Y-%m-%d.%H%M%S')

    exclude_file_name = os.path.join(options['config_dir'],
        options['backup_name'] + "-exclude")
    if os.path.isfile(exclude_file_name):
        exclude_string = "--exclude-from='%s'" % (exclude_file_name)
    else:
        exclude_string = ""

    current = os.path.join(options['destination'], "current")
    previous = os.path.join(options['destination'], "previous")
    dest = os.path.join(options['destination'], date_string)
    if os.path.exists(current):
        # Don't use -a as it preserves permissions and timestamps and group and ownership
        # and generally uses much more space than necessary
        command = ("rsync -av %s --link-dest='%s' '%s' '%s' && rm -f '%s' && mv -f '%s' '%s' && ln -f -s '%s' '%s'" % (exclude_string, current, options['source'], dest,
        previous, current, previous, date_string, current))
    else:
        command = ("rsync -av %s '%s' '%s' && ln -f -s '%s' '%s'" % (exclude_string, options['source'], dest, dest, current))

    return do_command(command, False)

def rdup_backup(options):

    import datetime

    date_string = datetime.datetime.now().strftime('%Y-%m-%d.%H%M')

    backup_file_name_base = os.path.join(options['destination'], "%s-%s" %
        (options['backup_name'], date_string))
    delete_file_name = os.path.join(options['destination'], "%s-%s.delete" %
        (options['backup_name'], date_string))
    rdup_up_directory = os.path.join(options['tmp'], 'rdup-up')
    dirsplit_directory = os.path.join(options['tmp'], 'dirsplit')
    dirsplit_output_prefix = os.path.join(dirsplit_directory, 'part-')
    delete_ctime_file_name = os.path.join(options['config_dir'],
        "%s-delete.ctime" % (options['backup_name']))
    modify_ctime_file_name = os.path.join(options['config_dir'],
        "%s-modify.ctime" % (options['backup_name']))
    filelist_file_name = os.path.join(options['config_dir'],
        "%s.filelist" % (options['backup_name']))

    print("Generating the delete file %s" % (delete_file_name))
    command = "rdup -r -N '%s' '%s' '%s' > '%s'" % (delete_ctime_file_name,
        filelist_file_name, options['source'], delete_file_name)
    return_value = do_command(command, False)
    if return_value != 0:
        return return_value

    print("Copying modified files to %s" % (rdup_up_directory))
    command = "rdup -m -N '%s' '%s' '%s' -c | rdup-up -t '%s'" % (
        modify_ctime_file_name, filelist_file_name, options['source'],
        rdup_up_directory)
    return_value = do_command(command, False)
    if return_value == 0:
        print("Splitting backup into %s-sized parts in %s*" %
            (options['splitsize'], dirsplit_output_prefix))
        command = "dirsplit -m -s %s -a 1 -p '%s' '%s'" % (options['splitsize'],
            dirsplit_output_prefix, rdup_up_directory)
        return_value = do_command(command, False)
        if return_value == 0:
            parts = os.listdir(dirsplit_directory)
            for part in parts:
                print("Tarring %s" % (part))
                command = "tar -c -C '%s' -f '%s-%s'.tar ." % (os.path.join(
                    dirsplit_directory, part), backup_file_name_base, part)
                return_value = do_command(command, False)
                if return_value != 0:
                    break

    print("Cleaning up -- deleting temporary files")
    import shutil
    shutil.rmtree(rdup_up_directory)
    shutil.rmtree(dirsplit_directory)
    return 0


def do_command(command, exit_on_error=True):
    return_value = os.system(command)
    if return_value != 0:
        print("%s: program failed with return value %d" %
            (progname, return_value))
        print("The command was\n%s" % (command))
        if exit_on_error:
            exit(2)
    return return_value

if __name__ == '__main__':
    exit_value = main()
    exit(exit_value)
